// Copyright (C) 2025, ZWSOFT Co., LTD.(Guangzhou)
// Licensed under the MIT License. See LICENSE in the project root for license information.

using System.Runtime.InteropServices;

namespace TestClient.Domain;
/// <summary>
/// Provides utility methods for marshalling string arrays to and from unmanaged BSTR arrays.
/// </summary>
public static class BSTRMarshaller
{
    /// <summary>
    /// Represents information about an allocated unmanaged BSTR array,
    /// including the pointer to the array and its length.
    /// </summary>
    public struct BSTRArrayInfo
    {
        /// <summary>
        /// A pointer to the unmanaged array of BSTR pointers (effectively BSTR**).
        /// </summary>
        public IntPtr NativeArrayPtr;

        /// <summary>
        /// The number of BSTRs in the unmanaged array.
        /// </summary>
        public int Length;
    }

    /// <summary>
    /// Allocates an unmanaged array of BSTR pointers from a managed string array.
    /// Each string in the input array is converted to a BSTR, and a pointer to that BSTR
    /// is stored in the newly allocated unmanaged array.
    /// </summary>
    /// <param name="managedStrings">The managed string array to convert to an unmanaged BSTR array.</param>
    /// <returns>
    /// A <see cref="BSTRArrayInfo"/> struct containing the pointer to the native array and its length.
    /// If <paramref name="managedStrings"/> is null or empty, an empty <see cref="BSTRArrayInfo"/> is returned.
    /// </returns>
    /// <remarks>
    /// It is crucial to call <see cref="ReleaseBSTRArray"/> on the returned <see cref="BSTRArrayInfo"/>
    /// when the unmanaged memory is no longer needed, to prevent memory leaks.
    /// </remarks>
    public static BSTRArrayInfo AllocateBSTRArray(string[] managedStrings)
    {
        BSTRArrayInfo info = new() { NativeArrayPtr = IntPtr.Zero, Length = 0 };

        // Handle null or empty input array
        if (managedStrings == null || managedStrings.Length == 0)
        {
            return info;
        }

        info.Length = managedStrings.Length;
        // Allocate memory for the array of BSTR pointers.
        // Each BSTR pointer is an IntPtr, so we need IntPtr.Size bytes per pointer.
        info.NativeArrayPtr = Marshal.AllocCoTaskMem(IntPtr.Size * info.Length);

        // Keep track of how many BSTRs have been successfully allocated
        // for proper cleanup in case of an exception during the loop.
        int allocatedBstrsCount = 0;

        try
        {
            for (int i = 0; i < info.Length; i++)
            {
                // Convert the managed string to an unmanaged BSTR.
                // Marshal.StringToBSTR allocates memory for the BSTR.
                IntPtr bstrPtr = Marshal.StringToBSTR(managedStrings[i]);

                // Write the pointer to the newly created BSTR into our native array.
                // The offset is `i * IntPtr.Size` because each element is an IntPtr.
                Marshal.WriteIntPtr(info.NativeArrayPtr, i * IntPtr.Size, bstrPtr);

                allocatedBstrsCount++; // Increment count only after successful allocation and write
            }
        }
        catch
        {
            // If an exception occurs (e.g., out of memory), clean up any BSTRs
            // and the native array that were successfully allocated before the error.
            for (int i = 0; i < allocatedBstrsCount; i++)
            {
                // Read the BSTR pointer from the native array at the current index.
                IntPtr bstrPtrToFree = Marshal.ReadIntPtr(info.NativeArrayPtr, i * IntPtr.Size);
                if (bstrPtrToFree != IntPtr.Zero)
                {
                    Marshal.FreeBSTR(bstrPtrToFree); // Free the individual BSTR
                }
            }
            if (info.NativeArrayPtr != IntPtr.Zero)
            {
                Marshal.FreeCoTaskMem(info.NativeArrayPtr); // Free the array of pointers
            }
            info.NativeArrayPtr = IntPtr.Zero; // Mark as freed
            info.Length = 0;
            throw; // Re-throw the original exception to notify the caller
        }

        return info;
    }

    /// <summary>
    /// Releases the unmanaged memory allocated for a BSTR array and all its contained BSTRs.
    /// This function should be called to clean up resources obtained from <see cref="AllocateBSTRArray"/>.
    /// </summary>
    /// <param name="arrayInfo">
    /// A reference to the <see cref="BSTRArrayInfo"/> struct obtained from <see cref="AllocateBSTRArray"/>.
    /// The struct's <see cref="BSTRArrayInfo.NativeArrayPtr"/> and <see cref="BSTRArrayInfo.Length"/>
    /// will be reset to indicate that the memory has been freed.
    /// </param>
    public static void ReleaseBSTRArray(ref BSTRArrayInfo arrayInfo)
    {
        // If the pointer is null or length is zero, there's nothing to release or it's already released.
        if (arrayInfo.NativeArrayPtr == IntPtr.Zero || arrayInfo.Length == 0)
        {
            return;
        }

        // Iterate through the unmanaged array and free each individual BSTR.
        for (int i = 0; i < arrayInfo.Length; i++)
        {
            // Read the BSTR pointer from the native array at the current index.
            IntPtr bstrPtr = Marshal.ReadIntPtr(arrayInfo.NativeArrayPtr, i * IntPtr.Size);
            if (bstrPtr != IntPtr.Zero)
            {
                Marshal.FreeBSTR(bstrPtr); // Free the individual BSTR
            }
        }

        // After freeing all individual BSTRs, free the memory allocated for the array of BSTR pointers itself.
        Marshal.FreeCoTaskMem(arrayInfo.NativeArrayPtr);

        // Reset the struct to indicate that the memory has been released,
        // preventing accidental double-free attempts.
        arrayInfo.NativeArrayPtr = IntPtr.Zero;
        arrayInfo.Length = 0;
    }
}
